#!/usr/bin/env python3
# coding=utf-8
# Copyright 2010-2021 Google LLC
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Creates a shift scheduling problem and solves it."""

#from sqlite3 import Row
from absl import app
from absl import flags
import json
import openpyxl
import os
from traceback import print_exc  # 调试用到的模块
#import eel
import flask
from datetime import datetime, timedelta
import webview

from ortools.sat.python import cp_model
from google.protobuf import text_format

FLAGS = flags.FLAGS

flags.DEFINE_string('output_proto', '',
                    'Output file to write the cp_model proto to.')
flags.DEFINE_string('params', 'max_time_in_seconds:10.0',
                    'Sat solver parameters.')
user_path = os.path.abspath('.')+"\\user_data\\"
# eel.init('web')
flask_app = flask.Flask(__name__)


@flask_app.route("/")
@flask_app.route('/index')
def index():
    file = open(os.path.abspath('.')+"\\static\\" + "index.html", encoding='utf-8', mode='r')
    index=file.read()
    return index

# @eel.expose

@flask_app.route('/dataFromJs',methods = ['POST'])
def dataFromJs():  # 程序执行入口
    req=flask.request
    data = req.json
    # print(data)
    # 写入json
    file = open(user_path + "data.json", encoding='utf-8', mode='w')
    file.write(json.dumps(data, ensure_ascii=False))
    file.close()
    try:
        app.run(main)
        return 
    except Exception as e:
        print_exc()
        return str(print_exc())


# @eel.expose
@flask_app.route('/template',methods = ['POST'])
def template():  # 生成模板
    req=flask.request
    data = req.json
    start_date=data["start_date"]
    #end_date=data["end_date"]
    num_employees=data["num_employees"]
    num_days = data["num_days"]
    try:
        # 字符串转日期
        startDate = datetime.strptime(start_date, '%Y-%m-%d')
        #endDate = datetime.strptime(end_date, '%Y-%m-%d')
        # 读取Excel
        wb = openpyxl.load_workbook(user_path + "排班模板.xlsx")
        origin_sheet = wb['原始排班']
        wb.remove(origin_sheet)
        wb.create_sheet("原始排班", 0)
        origin_sheet = wb['原始排班']
        for i in range(num_days):
            date = datetime.strftime(startDate + timedelta(i), '%Y-%m-%d')
            origin_sheet.cell(row=1, column=i+2).value = date
        for i in range(num_employees):
            origin_sheet.cell(row=i+2, column=1).value = "员工"+str(i)
        wb.save(user_path + "排班模板.xlsx")
        return '''已生成Excel模板，请按照以下步骤操作：\n
            1.请打开user_data文件夹下的排班模板文件`,\n
            2.在原始排班工作表中填入员工期望（字体颜色不是"自动"）和已固定的排班（单元格不是"无填充颜色"或纯白色）,\n
            3.确保填写正确后(特别是所填班种与在本界面上输入的班种一致)，关闭Excel文件继续进行下一步'''
    except Exception as e:
        print_exc()
        return str(print_exc())


# @eel.expose
@flask_app.route('/loadConfig',methods = ['POST','get'])
def loadConfig():
    file = open(user_path + "data.json", encoding='utf-8', mode='r')
    content = file.read()
    file.close()
    return content


def input_file():  # 读取文件
    # 读取json
    file = open(user_path + "data.json", encoding='utf-8', mode='r')
    input = file.read()
    input = json.loads(input)
    file.close()
    # 处理日期
    #startDate = datetime.strptime(input["start_date"], '%Y-%m-%d')
    #endDate = datetime.strptime(input["end_date"], '%Y-%m-%d')
    #input["num_days"] = (endDate-startDate).days+1
    # input["num_weeks"]
    # 读取Excel
    wb = openpyxl.load_workbook(user_path + "排班模板.xlsx")

    # 处理原始排班
    origin_sheet = wb['原始排班']
    output_sheet = wb['排班结果']
    wb.remove(output_sheet)
    wb.create_sheet("排班结果")
    output_sheet = wb['排班结果']
    # 处理原始排班-获取最大行列，原始数据复制粘贴,写入验证公式
    i = 2  # 行
    while origin_sheet.cell(row=i, column=1).value != None:
        output_sheet.cell(row=i, column=1).value = origin_sheet.cell(
            row=i, column=1).value
        i = i+1
    #output_sheet.cell(row=i, column=1).value = "早验证"
    #output_sheet.cell(row=i+1, column=1).value = "晚验证"
    #input["num_employees"] = i-2

    j = 2  # 列
    while origin_sheet.cell(row=1, column=j).value != None:
        output_sheet.cell(row=1, column=j).value = origin_sheet.cell(
            row=1, column=j).value
        output_sheet.cell(row=1, column=j).number_format = 'yy/mm/dd'
        # 验证公式
        '''column_letter = output_sheet.cell(row=1, column=j).column_letter
        output_sheet.cell(row=i, column=j).value = "=COUNTIF(" + \
            column_letter+"2:"+column_letter+str(i-1)+',"早")'
        output_sheet.cell(row=i+1, column=j).value = "=COUNTIF(" + \
            column_letter+"2:"+column_letter+str(i-1)+',"晚")'''
        j = j+1
    #input["num_days"] = j-2
    #input["num_weeks"] = input["num_days"]//7
    # 每月上班21天验证公式，默认前5天是已知数据
    '''column_letter = output_sheet.cell(
        row=1, column=input["num_days"]+1).column_letter
    output_sheet.cell(row=1, column=input["num_days"]+2).value = "验证上班21天"
    output_sheet.cell(row=1, column=input["num_days"]+3).value = "早班数量"
    for i in range(2, input["num_employees"]+2):
        output_sheet.cell(row=i, column=input["num_days"]+2).value = "=COUNTA(G"+str(i)+":"+column_letter+str(i)+')-' \
            + "COUNTIF(G"+str(i)+":"+column_letter+str(i)+',"休")'
        output_sheet.cell(
            row=i, column=input["num_days"]+3).value = "=COUNTIF(G"+str(i)+":"+column_letter+str(i)+',"早")'
    '''
    # 处理原始排班-获取固定排班和需求排班
    """注意单元格比数量大1，但是python的序号比数量小1"""
    for i in range(2, input["num_employees"]+2):
        for j in range(2, input["num_days"]+2):
            data = origin_sheet.cell(row=i, column=j)
            #print('%s %s %s' % (data.value,data.coordinate,data.font.color))
            if data.font.color != None and data.value != None:  # 员工需求
                requests = (
                    i-2, mohu_index(input["shifts"], data.value), j-2, -2)
                input["requests"].append(requests)
            if data.fill.fgColor.value != '00000000' and data.value != None:
                fixed_assignments = (
                    i-2, mohu_index(input["shifts"], data.value), j-2)
                input["fixed_assignments"].append(fixed_assignments)
    wb.save(user_path + "排班模板.xlsx")
    return input

# input_file中使用的列表模糊匹配，用于处理数据输入不规范


def mohu_index(list=[], str=""):
    try:
        index_num = list.index(str)
    except:
        str2 = str[0]
        index_num = list.index(str2)
    return index_num

# 输出结果


def output(num_employees, num_days, num_shifts, solver, work, shifts):
    wb = openpyxl.load_workbook(user_path+"排班模板.xlsx")
    output_sheet = wb['排班结果']
    # 排班结果
    for e in range(num_employees):
        for d in range(num_days):
            for s in range(num_shifts):
                if solver.BooleanValue(work[e, s, d]):
                    output_cell = output_sheet.cell(row=e+2, column=d+2)
                    output_cell.value = shifts[s]
                    # 为了便于验证，把年假底色换成红色f6283d，休换成黄色
                    if s == 1:
                        fill = openpyxl.styles.PatternFill(
                            "solid", fgColor="f6283d")
                        output_cell.fill = fill
                    elif s == 0:
                        fill = openpyxl.styles.PatternFill(
                            "solid", fgColor="f1f628")
                        output_cell.fill = fill
    wb.save(user_path+"排班模板.xlsx")

# 从这里开始主程序


def solve_shift_scheduling(params, output_proto):
    """Solves the shift scheduling problem."""

    # 读取文件
    input = input_file()
    # Data
    num_employees = input["num_employees"]  # 员工数
    num_weeks = input["num_weeks"]  # 需要排班的周数
    shifts = input["shifts"]  # 班种
    fixed_assignments = input["fixed_assignments"]  # 固定排班
    requests = input["requests"]  # 员工需求
    shift_constraints = input["shift_constraints"]    # 连续上某班种的天数限制
    weekly_sum_constraints = input["weekly_sum_constraints"]  # 每周上某班种的天数限制
    days_sum_constraints = input["days_sum_constraints"]  # 每n天上某班种的天数限制
    penalized_transitions = input["penalized_transitions"]  # 连续两天之间班种转换的罚值:
    # 一周中各天各个班种需要的员工数,休息不计算在内(第一个班种)
    weekly_cover_demands = input["weekly_cover_demands"]
    # 一周中各天各个班种需要的员工数（最大值）
    #weekly_cover_demands_max = input["weekly_cover_demands_max"]
    excess_cover_penalties = input["excess_cover_penalties"]   # 超过上条中规定数量的罚值
    num_days = input["num_days"]  # num_weeks * 7  # 需要排班的天数

    del input
    num_shifts = len(shifts)

    model = cp_model.CpModel()
    """Methods for building and solving CP-SAT models.

    The following two sections describe the main methods for building and solving CP-SAT models.

    [CpModel](#cp_model.CpModel): Methods for creating models, including variables and constraints.
    [CPSolver](#cp_model.CpSolver): Methods for solving a model and evaluating solutions.
    The following methods implement callbacks that the solver calls each time it finds a new solution.

    [CpSolverSolutionCallback](#cp_model.CpSolverSolutionCallback): A general method for implementing callbacks.
    [ObjectiveSolutionPrinter](#cp_model.ObjectiveSolutionPrinter): Print objective values and elapsed time for intermediate solutions.
    [VarArraySolutionPrinter](#cp_model.VarArraySolutionPrinter): Print intermediate solutions (variable values, time).
    [VarArrayAndObjectiveSolutionPrinter] (#cp_model.VarArrayAndObjectiveSolutionPrinter): Print both intermediate solutions and objective values.
    Additional methods for solving CP-SAT models:

    [Constraint](#cp_model.Constraint): A few utility methods for modifying constraints created by CpModel.
    [LinearExpr](#lineacp_model.LinearExpr): Methods for creating constraints and the objective from large arrays of coefficients.
    Other methods and functions listed are primarily used for developing OR-Tools, rather than for solving specific optimization problems.
    """
    work = {}
    # 创建【员工，班种，日期】的三维0-1数组
    for e in range(num_employees):
        for s in range(num_shifts):
            for d in range(num_days):
                work[e, s, d] = model.NewBoolVar('work%i_%i_%i' % (e, s, d))

    # Linear terms of the objective in a minimization context.
    obj_int_vars = []
    obj_int_coeffs = []
    obj_bool_vars = []
    obj_bool_coeffs = []

    # Exactly one shift per day.
    # 保证每天有且只有一个排班
    # 每天每人班种和为1
    for e in range(num_employees):
        for d in range(num_days):
            model.Add(sum(work[e, s, d] for s in range(num_shifts)) == 1)

    # Fixed assignments.
    # 已固定的排班
    # 三维数组中选定位置值永远为1
    for e, s, d in fixed_assignments:
        model.Add(work[e, s, d] == 1)

    # Employee requests
    # 员工需求
    for e, s, d, w in requests:
        obj_bool_vars.append(work[e, s, d])
        obj_bool_coeffs.append(w)

    # Shift constraints
    # 连续上某班种的天数限制,本次未添加
    for ct in shift_constraints:
        shift, hard_min, soft_min, min_cost, soft_max, hard_max, max_cost = ct
        for e in range(num_employees):
            works = [work[e, shift, d] for d in range(num_days)]
            variables, coeffs = add_soft_sequence_constraint(
                model, works, hard_min, soft_min, min_cost, soft_max, hard_max,
                max_cost,
                'shift_constraint(employee %i, shift %i)' % (e, shift))
            obj_bool_vars.extend(variables)
            obj_bool_coeffs.extend(coeffs)

    # 新加，除前5天外，一共21天上班限制
    '''for e in range(num_employees):
        model.Add(sum(work[e, 0, d]
                  for d in range(5, num_days)) == num_days-21-5)
        # 早班在5到7之间
        hard_min = int(round(num_days*5/num_employees, 0))-1
        hard_max = int(round(num_days*5/num_employees, 0))+1
        model.Add(sum(work[e, 2, d]
                  for d in range(5, num_days)) >= hard_min)
        model.Add(sum(work[e, 2, d]
                  for d in range(5, num_days)) <= hard_max)'''

    # 新加，每7天中必须有1天休息，最好2天
    # days_sum_constraints,改自模块Weekly sum constraints
    # (days,shift, hard_min, soft_min, min_penalty,
    #             soft_max, hard_max, max_penalty)
    for ct in days_sum_constraints:
        days, shift, hard_min, soft_min, min_cost, soft_max, hard_max, max_cost = ct
        for e in range(num_employees):
            for d in range(num_days-days):
                works = [work[e, shift, d + i] for i in range(days)]
                variables, coeffs = add_soft_sum_constraint(
                    model, works, hard_min, soft_min, min_cost, soft_max,
                    hard_max, max_cost,
                    'days_sum_constraint(employee %i, shift %i, day %i)' %
                    (e, shift, d))
                obj_int_vars.extend(variables)
                obj_int_coeffs.extend(coeffs)

    # Weekly sum constraints
    # 每周上某班种的天数限制(本次未添加)
    for ct in weekly_sum_constraints:
        shift, hard_min, soft_min, min_cost, soft_max, hard_max, max_cost = ct
        for e in range(num_employees):
            for w in range(num_weeks):
                works = [work[e, shift, d + w * 7] for d in range(7)]
                variables, coeffs = add_soft_sum_constraint(
                    model, works, hard_min, soft_min, min_cost, soft_max,
                    hard_max, max_cost,
                    'weekly_sum_constraint(employee %i, shift %i, week %i)' %
                    (e, shift, w))
                obj_int_vars.extend(variables)
                obj_int_coeffs.extend(coeffs)

    '''# 总共上某班种的天数限制，新加，自动计算得出参数
    #早班
    soft_min=int(round(num_days*5/num_employees,0))-1
    soft_max=int(round(num_days*5/num_employees,0))+1
    sum_constraints=[(2,soft_min,soft_min,5,soft_max,soft_max,5)]
    for ct in sum_constraints:
        shift, hard_min, soft_min, min_cost, soft_max, hard_max, max_cost = ct
        for e in range(num_employees):
            #for w in range(num_weeks):
                works = [work[e, shift, d] for d in range(num_days)]
                variables, coeffs = add_soft_sum_constraint(
                    model, works, hard_min, soft_min, min_cost, soft_max,
                    hard_max, max_cost,
                    'sum_constraint(employee %i, shift %i)' %
                    (e, shift))
                obj_int_vars.extend(variables)
                obj_int_coeffs.extend(coeffs)'''

    # Penalized transitions
    # 连续两天之间班种转换的罚值
    for previous_shift, next_shift, cost in penalized_transitions:
        for e in range(num_employees):
            for d in range(num_days - 1):
                transition = [
                    work[e, previous_shift, d].Not(), work[e, next_shift,
                                                           d + 1].Not()
                ]
                if cost == 0:  # 0表示禁止
                    model.AddBoolOr(transition)
                else:
                    trans_var = model.NewBoolVar(
                        'transition (employee=%i, day=%i)' % (e, d))
                    transition.append(trans_var)
                    model.AddBoolOr(transition)
                    obj_bool_vars.append(trans_var)
                    obj_bool_coeffs.append(cost)

    # Cover constraints
    # 一周中各天各个班种需要的员工数
    for s in range(num_shifts):  # Ignore Off shift.忽略休息限制(原为range(1,num_shifts))
        for w in range(num_weeks+1):  # +1为处理最后一周不满7天的情况
            for d in range(7):
                if w == num_weeks and d >= (num_days-num_weeks*7):  # 最后一周多的天数直接跳过
                    continue
                works = [work[e, s, w * 7 + d] for e in range(num_employees)]
                # Ignore Off shift.忽略休息限制(原为s-1)
                min_demand = weekly_cover_demands[d][s]
                #max_demand = weekly_cover_demands_max[d][s - 1]
                worked = model.NewIntVar(min_demand, num_employees, '')
                #worked_max = model.NewIntVar(max_demand, num_employees, '')
                model.Add(worked == sum(works))
                #model.Add(worked_max >= sum(works))
                over_penalty = excess_cover_penalties[s]
                if over_penalty > 0:
                    name = 'excess_demand(shift=%i, week=%i, day=%i)' % (s, w,
                                                                         d)
                    excess = model.NewIntVar(0, (num_employees - min_demand),
                                             name)
                    model.Add(excess == worked - min_demand)
                    obj_int_vars.append(excess)
                    obj_int_coeffs.append(over_penalty)

    # Objective
    model.Minimize(
        sum(obj_bool_vars[i] * obj_bool_coeffs[i]
            for i in range(len(obj_bool_vars))) +
        sum(obj_int_vars[i] * obj_int_coeffs[i]
            for i in range(len(obj_int_vars))))

    if output_proto:
        print('Writing proto to %s' % output_proto)
        with open(output_proto, 'w') as text_file:
            text_file.write(str(model))

    # Solve the model.
    solver = cp_model.CpSolver()
    if params:
        text_format.Parse(params, solver.parameters)
    solution_printer = cp_model.ObjectiveSolutionPrinter()
    status = solver.Solve(model, solution_printer)

    # Print solution.
    # 输出结果
    if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
        output(num_employees, num_days, num_shifts, solver, work, shifts)
        print("结果预览")

        header = '          '
        for w in range(num_weeks):
            header += '周一 周二 周三 周四 周五 周六 周日 '
        print(header)

        for e in range(num_employees):
            schedule = ''
            for d in range(num_days):
                for s in range(num_shifts):
                    if solver.BooleanValue(work[e, s, d]):
                        schedule += shifts[s] + ' '

            print('员工 %i: %s' % (e, schedule))
        print()

        print('罚值处理:')
        for i, var in enumerate(obj_bool_vars):
            if solver.BooleanValue(var):
                penalty = obj_bool_coeffs[i]
                if penalty > 0:
                    print('  %s 违反, penalty=%i' % (var.Name(), penalty))
                else:
                    print('  %s 完成, gain=%i' % (var.Name(), -penalty))

        for i, var in enumerate(obj_int_vars):
            if solver.Value(var) > 0:
                print('  %s violated by %i, linear penalty=%i' %
                      (var.Name(), solver.Value(var), obj_int_coeffs[i]))

    # print()
    print('统计数据')
    print('  - 状态       : %s' % solver.StatusName(status))
    print("  -     OPTIMAL 代表最优解")
    print('  -     INFEASIBLE 代表无解')
    print('  -     FEASIBLE 代表可行解')
    print('  - 冲突       : %i' % solver.NumConflicts())
    print('  - 分支       : %i' % solver.NumBranches())
    print('  - 运行时长   : %f s' % solver.WallTime())
    return


def negated_bounded_span(works, start, length):
    """Filters an isolated sub-sequence of variables assined to True.
    构建子序列，
  Extract the span of Boolean variables [start, start + length), negate them,
  and if there is variables to the left/right of this span, surround the span by
  them in non negated form.

  Args:
    works: a list of variables to extract the span from.
    start: the start to the span.
    length: the length of the span.

  Returns:
    a list of variables which conjunction will be false if the sub-list is
    assigned to True, and correctly bounded by variables assigned to False,
    or by the start or end of works.
  """
    sequence = []
    # Left border (start of works, or works[start - 1])
    if start > 0:
        sequence.append(works[start - 1])
    for i in range(length):
        sequence.append(works[start + i].Not())
    # Right border (end of works or works[start + length])
    if start + length < len(works):
        sequence.append(works[start + length])
    return sequence


def add_soft_sequence_constraint(model, works, hard_min, soft_min, min_cost,
                                 soft_max, hard_max, max_cost, prefix):
    """Sequence constraint on true variables with soft and hard bounds.
        对含有软性和硬性边界的变量进行连续约束
  This constraint look at every maximal contiguous sequence of variables
  assigned to true. If forbids sequence of length < hard_min or > hard_max.
  Then it creates penalty terms if the length is < soft_min or > soft_max.

  Args:参数：
    model: the sequence constraint is built on this model.
    连续约束是建立在此模型上
    works: a list of Boolean variables.
    hard_min: any sequence of true variables must have a length of at least
      hard_min.
    soft_min: any sequence should have a length of at least soft_min, or a
      linear penalty on the delta will be added to the objective.
    min_cost: the coefficient of the linear penalty if the length is less than
      soft_min.
    soft_max: any sequence should have a length of at most soft_max, or a linear
      penalty on the delta will be added to the objective.
    hard_max: any sequence of true variables must have a length of at most
      hard_max.
    max_cost: the coefficient of the linear penalty if the length is more than
      soft_max.
    prefix: a base name for penalty literals.
    前缀：惩罚文字的根名字

  Returns:
    a tuple (variables_list, coefficient_list) containing the different
    penalties created by the sequence constraint.
    一个由连续约束创建的包含不同罚值的元组（变量列表和系数列表）
  """
    cost_literals = []
    cost_coefficients = []

    # Forbid sequences that are too short.
    for length in range(1, hard_min):
        for start in range(len(works) - length + 1):
            model.AddBoolOr(negated_bounded_span(works, start, length))

    # Penalize sequences that are below the soft limit.
    if min_cost > 0:
        for length in range(hard_min, soft_min):
            for start in range(len(works) - length + 1):
                span = negated_bounded_span(works, start, length)
                name = ': under_span(start=%i, length=%i)' % (start, length)
                lit = model.NewBoolVar(prefix + name)
                span.append(lit)
                model.AddBoolOr(span)
                cost_literals.append(lit)
                # We filter exactly the sequence with a short length.
                # The penalty is proportional to the delta with soft_min.
                cost_coefficients.append(min_cost * (soft_min - length))

    # Penalize sequences that are above the soft limit.
    if max_cost > 0:
        for length in range(soft_max + 1, hard_max + 1):
            for start in range(len(works) - length + 1):
                span = negated_bounded_span(works, start, length)
                name = ': over_span(start=%i, length=%i)' % (start, length)
                lit = model.NewBoolVar(prefix + name)
                span.append(lit)
                model.AddBoolOr(span)
                cost_literals.append(lit)
                # Cost paid is max_cost * excess length.
                cost_coefficients.append(max_cost * (length - soft_max))

    # Just forbid any sequence of true variables with length hard_max + 1
    for start in range(len(works) - hard_max):
        model.AddBoolOr(
            [works[i].Not() for i in range(start, start + hard_max + 1)])
    return cost_literals, cost_coefficients


def add_soft_sum_constraint(model, works, hard_min, soft_min, min_cost,
                            soft_max, hard_max, max_cost, prefix):
    """Sum constraint with soft and hard bounds.
  汇总具有软边界和硬边界的约束条件
  This constraint counts the variables assigned to true from works.
  If forbids sum < hard_min or > hard_max.
  Then it creates penalty terms if the sum is < soft_min or > soft_max.
  这个约束条件对工作的ture变量进行计数，禁止超出硬边界，超出软边界赋予罚值
  Args:参数
    model: the sequence constraint is built on this model.
    works: a list of Boolean variables.
    hard_min: any sequence of true variables must have a sum of at least
      hard_min.
    soft_min: any sequence should have a sum of at least soft_min, or a linear
      penalty on the delta will be added to the objective.
    min_cost: the coefficient of the linear penalty if the sum is less than
      soft_min.
    soft_max: any sequence should have a sum of at most soft_max, or a linear
      penalty on the delta will be added to the objective.
    hard_max: any sequence of true variables must have a sum of at most
      hard_max.
    max_cost: the coefficient of the linear penalty if the sum is more than
      soft_max.
    prefix: a base name for penalty variables.

  Returns:
    a tuple (variables_list, coefficient_list) containing the different
    penalties created by the sequence constraint.
    包含不同罚值的元组
  """
    cost_variables = []
    cost_coefficients = []
    sum_var = model.NewIntVar(hard_min, hard_max, '')
    # This adds the hard constraints on the sum.
    model.Add(sum_var == sum(works))

    # Penalize sums below the soft_min target.
    if soft_min > hard_min and min_cost > 0:
        delta = model.NewIntVar(-len(works), len(works), '')
        model.Add(delta == soft_min - sum_var)
        # TODO(user): Compare efficiency with only excess >= soft_min - sum_var.
        excess = model.NewIntVar(0, len(works), prefix + ': under_sum')
        model.AddMaxEquality(excess, [delta, 0])
        cost_variables.append(excess)
        cost_coefficients.append(min_cost)

    # Penalize sums above the soft_max target.
    if soft_max < hard_max and max_cost > 0:
        delta = model.NewIntVar(-len(works), len(works), '')
        model.Add(delta == sum_var - soft_max)
        excess = model.NewIntVar(0, len(works), prefix + ': over_sum')
        model.AddMaxEquality(excess, [delta, 0])
        cost_variables.append(excess)
        cost_coefficients.append(max_cost)

    return cost_variables, cost_coefficients


def main(_):
    solve_shift_scheduling(FLAGS.params, FLAGS.output_proto)
    # 调试用
    # input("按任意键退出")


#eel.start('index.html', mode='edge', port=8000)
if __name__ == '__main__':
    #flask_app.run(debug=True)
    window = webview.create_window('排班系统', flask_app)
    webview.start(debug=True)
"""if __name__ == '__main__':
    try:
        app.run(main)
    except Exception as e:
        print_exc()"""
